<?php
/** 
 * FileFind - ftp/shares indexing and search php engine,
 * http://project.ffind.ru
 *
 * Module: lib.table_struct.php
 * Description: Table structure generation
 *
 * @package filefind
 * @copyright (c) 2003-2008 by Rage Steel, www.gt2.ru
 * @version $Id$
 */

// На очереди - комментарии к строкам
// Внешние индексы
// Типы таблиц


// WARNING!!! Пока бы знаем только о тех типах, что реально используются в filefind!
$known_types = array(
  'BIGINT' => array(
    'numeric' => true
  ),
  'INT' => array(
    'numeric' => true
  ),
  'MEDIUMINT' => array(
    'numeric' => true
  ),
  'SMALLINT' => array(
    'numeric' => true
  ),
  'TINYINT' => array(
    'numeric' => true
  ),
  'DATETIME' => array(
    'numeric' => false
  ),
  'DATE' => array(
    'numeric' => false
  ),
  'TIMESTAMP' => array(
    'numeric' => false
  ),
  'TIME' => array(
    'numeric' => false
  ),
  'TINYTEXT' => array(
    'numeric' => false
  ),
  'TEXT' => array(
    'numeric' => false
  ),
  'VARCHAR' => array(
    'numeric' => false
  ),
  'CHAR' => array(
    'numeric' => false
  )
);

function export_table_data($table) {
  global $known_types;

  // *** Сначала поля таблиц
  $fields = array();
  $sq = new CSqlQ($table);
  $sq->exec_sql('SHOW FULL FIELDS FROM `' . $sq->table);
  while($table_fields = $sq->fetch_assoc()) {
    $field = array();

    // Тип
    preg_match('/^(\w+)(\(\d+\))?( \w+)?$/i', $table_fields['Type'], $type_m);
    $type = strtoupper($type_m[1]);
    if ('' == $type) {
      die('export_table_data: Cannot detect type!');
    }
    if (!isset($known_types[$type])) {
      die('export_table_data: Unknown type: ' . $type);
    } else {
      $cur_type = $known_types[$type];
    }
    $field['type'] = $type;

    // Размер
    if (isset($type_m[2])) {
      $field['size'] = (int)substr($type_m[2], 1, -1);
    }

    // Со знаком или без
    if (isset($type_m[3])) {
      preg_match('/( unsigned)?/', $type_m[3], $opt_m);
      if (isset($opt_m[1])) {
        if ($opt_m[1] == ' unsigned') {
          $field['unsigned'] = true;
        }
      }
    }

    // Допустимы ли пустые значения
    $field['is_null'] = ('YES' == $table_fields['Null']) ? true : false;

    // Значение по умолчанию
    $default = $table_fields['Default'];
    if ($default != NULL) {
      if ($cur_type['numeric']) {
        $default = (int)$default;
      }
      $field['default'] = $default;
    }

    // Автоинкрементное-ли поле
    if ('auto_increment' == $table_fields['Extra']) {
      $field['auto_increment'] = true;
    }

    // Комментарий
    if (isset($table_fields['Comment'])) {
      $comment = $table_fields['Comment'];
      if ($comment <> '') {
        $field['comment'] = $comment;
      }
    }

    $fields[$table_fields['Field']] = $field;
  }
  
  // *** Индексы
  $indexes = array();
  $sq->exec_sql('SHOW INDEX FROM `' . $sq->table . '`');
  while(list($table, $non_unique, $key_name, $seq_in_index, $column_name, $collation, 
    $cardinality, $sub_part, $packed, $null, $index_type, $comment) = $sq->fetch_row()) {

    if (!isset($indexes[$key_name])) {
      $indexes[$key_name] = array();
      if ($key_name != 'PRIMARY') {
        $indexes[$key_name]['type'] = $non_unique ? 'INDEX' : 'UNIQUE';
      }
    }

    if (!isset($indexes[$key_name]['fields'])) {
      $indexes[$key_name]['fields'] = array(
        $column_name => $sub_part
      );
    } else {
      $indexes[$key_name]['fields'][$column_name] = $sub_part;
    }
  }

  // *** Комментарий
  $sq->exec_sql('SHOW TABLE STATUS LIKE "' . $sq->table . '"');
  $table_status = $sq->fetch_assoc();
  if (!isset($table_status['Comment'])) {
    die('Comment field in result of SHOW TABLE STATUS not found!');
  }
  $comment = $table_status['Comment'];

  /*
  list($name, $type, $row_format, $rows, $avg_row_length, $data_length, $max_data_length, 
    $index_length, $data_free, $auto_increment, $create_time, $update_time, 
    $check_time, $create_options, $comment) = $sq->fetch_row();
  */
  // TODO Связи таблиц из PMA / таблиц InnoDB

  return array(
    'fields' => $fields,
    'indexes' => $indexes,
    'comment' => $comment
  );
} // export_table_data

// Сгенерировать структуру таблиц
function gen_table_struct() {
  global $cfg;

  $table_struct = array();

  $exist_tables = array();
  $sq = new CSqlQ();
  $sq->exec_sql('SHOW TABLES');
  while(list($table_name) = $sq->fetch_row()) {
    $exist_tables[] = $table_name;
  }

  reset($cfg['all_tables']);
  while(list(, $tbl) = each($cfg['all_tables'])) {
    if (in_array($cfg[$tbl], $exist_tables)) {
      $table_struct[$tbl] = export_table_data($tbl);
    } else {
      echo "Warning: table $tbl does not exists\n";
    }
  }

  return $table_struct;
} // get_table_struct()

// Проверка на равенство полей таблиц, на входе - массивы сравниваемых полей
function is_field_equals($f1, $f2) {
  $result = ($f1['type'] == $f2['type']) && ($f1['is_null'] == $f2['is_null']);
  if (!$result) {
    return false;
  }

  $result = ($has_size = isset($f1['size'])) == isset($f2['size']);
  if (!$result) {
    return false;
  }
  if ($has_size) {
    $result = $f1['size'] == $f2['size'];
    if (!$result) {
      return false;
    }
  }

  $result = ($has_default = isset($f1['default'])) == isset($f2['default']);
  if (!$result) {
    return false;
  }
  if ($has_default) {
    $result = $f1['default'] == $f2['default'];
    if (!$result) {
      return false;
    }
  }

  if (isset($f1['auto_increment']) != isset($f2['auto_increment'])) {
    return false;
  }

  if (isset($f1['unsigned']) != isset($f2['unsigned'])) {
    return false;
  }

  if (isset($f1['comment']) != isset($f2['comment'])) {
    return false;
  }

  if (isset($f1['comment'])) {
    if ($f1['comment'] != $f2['comment']) {
      return false;
    }
  }

  return true;
} // is_field_equals

// Проверка на равенство индексов
function is_index_equals($i1, $i2) {
  $result = (isset($i1['type']) == isset($i2['type']));
  if (!$result) {
    return false;
  }

  if (isset($i1['type'])) {
    $result = $i1['type'] == $i2['type'];

    if (!$result) {
      return false;
    }
  }

  $result = count($i1['fields']) == count($i2['fields']);
  if (!$result) {
    return false;
  }

  reset($i1['fields']);
  reset($i2['fields']);
  while(true) {
    list($f1_name, $f1_sub_part) = each($i1['fields']);
    list($f2_name, $f2_sub_part) = each($i2['fields']);
    $result = ($f1_name == $f2_name) && ($f1_sub_part === $f2_sub_part);
    if (!$result) {
      return false;
    }

    if (!next($i1['fields'])) {
      break;
    }
    next($i2['fields']);
  }

  return true;
} // is_index_equals

// Сгенерировать определение определение
// f_name - имя колонки
// f_data - параметры колонки
function gen_field_def($f_name, $f_data) {
  global $sqlconn;

  $result = '`' . $f_name . '` ' . $f_data['type'];

  if (isset($f_data['size'])) {
    $result .= '(' . (int)$f_data['size'] . ')';
  }

  if (isset($f_data['unsigned'])) {
    $result .= ' UNSIGNED';
  }

  if ($f_data['is_null']) {
    $result .= ' NULL';
  } else {
    $result .= ' NOT NULL';
  }

  if (isset($f_data['default'])) {
    $result .= ' DEFAULT';
    $default = $f_data['default'];

    if (is_int($default)) {
      $result .= ' ' . $default;
    } else {
      $result .= ' "' . $sqlconn->escape($default). '"';
    }
  }

  if (isset($f_data['auto_increment'])) {
    $result .= ' AUTO_INCREMENT';
  }

  if (isset($f_data['comment'])) {
    $result .= ' COMMENT "' . $sqlconn->escape($f_data['comment']) . '"';
  }

  return $result;
} // gen_field_def

function gen_index_def($i_name, $i_data) {
  if ('PRIMARY' == $i_name) {
    $result = 'PRIMARY KEY';
  } else {
    if (!isset($i_data['type'])) {
      die("No index type defined for $i_name!\n");
    }
    $i_type = $i_data['type'];
    if (('UNIQUE' == $i_type) || ('INDEX' == $i_type)) {
      $result = $i_type;
    } else {
      die("Unknown undex type: $i_type!\n");
    }
    $result .= ' `' . $i_name . '`';
  }

  $r_fields = array();

  $i_fields = $i_data['fields'];
  reset($i_fields);

  while(list($i_field, $i_sub_part) = each($i_fields)) {
    $i_field = '`' . $i_field . '`';
    if (is_null($i_sub_part)) {
      $r_fields[] = $i_field;
    } else {
      $r_fields[] = $i_field . '(' . $i_sub_part . ')';
    }
  }

  return $result . ' (' . implode(', ', $r_fields) . ')';
} // gen_index_def

function gen_drop_index($i_name) {
  if ('PRIMARY' == $i_name) {
    return 'DROP PRIMARY KEY';
  } else {
    return 'DROP INDEX `' . $i_name . '`';
  }
} // gen_drop_index

function gen_add_index($i_name, $i_data) {
  return 'ADD ' . gen_index_def($i_name, $i_data);
} // gen_add_index

// Синхронизовать структуру таблиц
function sync_table_struct($master, $slave) {
  global $cfg;

  function do_create_table($tbl, $master) {
    global $sqlconn;

    $sq = new CSqlQ($tbl);

    $create = 'CREATE TABLE `' . $sq->table . '`' . " (\n";

    $create_defs = array();

    $m_fields = $master['fields'];
    reset($m_fields);
    while(list($mf, $mf_def) = each($m_fields)) {
      $create_defs[] = gen_field_def($mf, $mf_def);
    }

    $i_fields = $master['indexes'];
    reset($i_fields);
    while(list($mi, $mi_def) = each($i_fields)) {
      $create_defs[] = gen_index_def($mi, $mi_def);
    }

    $create .= '  ' . implode(",\n  ", $create_defs) . "\n) " .
      'COMMENT = "' . $sqlconn->escape($master['comment']) . '";' .
      "\n";

    return $create;
  } // do_create_table

  // Синхронизации одной таблицы
  function do_sync_table($tbl, $master, $slave) {
    global $sqlconn;

    $sq = new CSqlQ($tbl);
    $alter = 'ALTER TABLE `' . $sq->table . '`' . "\n  ";
    $alter_arr = array();

    // *** Поля
    $m_fields = $master['fields'];
    $s_fields = $slave['fields'];

    // Синхронизация списка полей и расположения полей списка --- всё в одном цилке

    // Имена полей в мастере
    $mf_names = array_keys($m_fields); $mi = 0;
    // Имена полей в слейве, "отработанные" имена удаляются из этого массива
    $sf_names = array_keys($s_fields);
    // Порядок колонок относительно друг-друга
    $col_order = ' FIRST';

    // Сначала надо удалить "лишние" поля в слейве
    reset($sf_names);
    while(list($si, $sf) = each($sf_names)) {
      if (!isset($m_fields[$sf])) {
        $alter_arr[] = 'DROP COLUMN `' . $sf . '`';
        unset($sf_names[$si]);
      }
    }

    while(true) {
      $mf = $mf_names[$mi];

      if (count($sf_names) == 0) {
        // Закончились поля в слейве
        $si = -1;
      } else {
        reset($sf_names);
        $si = key($sf_names);
      }
      $sf = ($si == -1) ? NULL : $sf_names[$si];
    
      if ($mf == $sf) {
        // Всё хорошо, имена полей совпадают
        if (!is_field_equals($m_fields[$mf], $s_fields[$mf])) {
          // Но не совпадают данные о полях
          $alter_arr[] = 'MODIFY COLUMN ' . gen_field_def($mf, $m_fields[$mf]);
        }
        unset($sf_names[$si]);
      } elseif(in_array($mf, $sf_names)) {
        // Поле есть в слейве, но оно стоит не на том месте
        $si = array_search($mf, $sf_names);
        // Надо передвинуть поле на новое место
        $alter_arr[] = 'MODIFY COLUMN ' . gen_field_def($mf, $m_fields[$mf]) . $col_order;
        unset($sf_names[$si]);
      } else {
        // Поля нет в слейве, надо его добавить
        $alter_arr[] = 'ADD COLUMN ' . gen_field_def($mf, $m_fields[$mf]) . $col_order;
      }

      $mi++;
      if (!isset($mf_names[$mi])) {
        // Выходим из цикла --- все колонки мастера прошли
        break;
      } else {
        // Мы продолжаем
        $col_order = ' AFTER `' . $mf . '`';
      }
    }

    // *** Индексы
    // С индексами вроде всё просто. Порядок там не важен.
    // Важно совпадают-ли индексы полностью, если нет - бросить и создать заново.
    $m_indexes = $master['indexes'];
    $s_indexes = $slave['indexes'];

    $mi_names = array_keys($m_indexes); $mii = 0;
    $si_names = array_keys($s_indexes);

    // Добавить недостающие индексы
    $add_indexes = array_diff($mi_names, $si_names);
    reset($add_indexes);
    while(list(, $i_name) = each($add_indexes)) {
      $alter_arr[] = gen_add_index($i_name, $m_indexes[$i_name]);
    }

    // Удалить лишиние индексы в слейве
    $drop_indexes = array_diff($si_names, $mi_names);
    reset($drop_indexes);
    while(list(, $i_name) = each($drop_indexes)) {
      $alter_arr[] = gen_drop_index($i_name);
    }

    // Проверить соответствие пересекающихся индексов
    $check_indexes = array_intersect($mi_names, $si_names);
    reset($check_indexes);
    while(list(, $i_name) = each($check_indexes)) {
      if (!is_index_equals($m_indexes[$i_name], $s_indexes[$i_name])) {
        $alter_arr[] = gen_drop_index($i_name);
        $alter_arr[] = gen_add_index($i_name, $m_indexes[$i_name]);
      }
    }

    // *** Комментарии
    if ($master['comment'] != $slave['comment']) {
      $alter_arr[] = 'COMMENT = "' . $sqlconn->escape($master['comment']) . '"';
    }

    if (count($alter_arr) > 0 ) {
      return $alter . implode(",\n  ", $alter_arr) . ";\n";
    } else {
      return '# table ' . $sq->table . " is match\n";
    }
  } // do_sync_table

  reset($cfg['all_tables']);
  while(list(, $tbl) = each($cfg['all_tables'])) {
    if (!isset($slave[$tbl])) {
      echo do_create_table($tbl, $master[$tbl]);
    } elseif (!isset($master[$tbl])) {
      echo 'DROP TABLE `' . $cfg[$tbl] . "`;\n";
    } else {
      echo do_sync_table($tbl, $master[$tbl], $slave[$tbl]);
    }
  }
} // sync_table_struct

?>